#! /usr/bin/env python3

import argparse
import glob
import hashlib
import os
import subprocess
import sys

if sys.version_info < (3, 6):
  print("Error: Python >= 3.6 is required, current version is ", sys.version)
  exit(1)

def hash_string(to_hash):
  return hashlib.md5(to_hash.encode('utf-8')).hexdigest()

def build_docker_image(no_cache=False):
  dockerfile_string = """
"""  
  # li setup: build the docker image.
  dockerfile = f"""
FROM buildpack-deps:focal
RUN apt-get update && apt-get install -yqq libboost-context-dev libboost-dev wget libmariadb-dev\
            postgresql-server-dev-12 libpq-dev cmake
RUN git clone https://github.com/matt-42/lithium.git /lithium
RUN /lithium/install.sh /usr"""

  print(f"Building lithium docker image.")

  no_cache_flag = "--no-cache " if no_cache else ""

  subprocess.run(f"docker build {no_cache_flag}-t lithium_docker_image -".split(" "), input=dockerfile.encode('utf-8'), check=True)

def lithium_docker_image_exists():
  return subprocess.run("docker image inspect lithium_docker_image".split(" "), stdout=open(os.devnull, 'wb')).returncode == 0
  
def build_docker_image_if_missing():
  ret = subprocess.run("docker image inspect lithium_docker_image".split(" "), stdout=open(os.devnull, 'wb')).returncode
  if not lithium_docker_image_exists():
    build_docker_image();


SERVER_PROCESS=None

def sigint_handler(signum, frame):
    print('Signal handler called with signal', signum)
    if SERVER_PROCESS is not None:
      print('FORWARD signal to process ', SERVER_PROCESS.pid)    
      SERVER_PROCESS.terminate();
    exit(1)


def create_cmake_script_if_needed(source_files, output_dir):
  
  output_filepath = os.path.join(output_dir, "CMakeLists.txt")
  hash = hash_string(" ".join(source_files))

  # Test if we need to regenerate the CMakeLists.txt
  if os.path.exists(output_filepath):
    with open(output_filepath) as f:
      previous_hash = f.readline().replace("# ", "")
      if hash == previous_hash:
        print("Skip CMakeLists.txt generation.")
        return

  script = """
# {hash}
cmake_minimum_required(VERSION 3.0)
set(CMAKE_CXX_STANDARD 17)
SET(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} /lithium/cmake)

project(lithium_server)

find_package(MYSQL REQUIRED)
find_package(SQLite3 REQUIRED)
find_package(CURL REQUIRED)
find_package(Threads REQUIRED)
find_package(PostgreSQL REQUIRED)
find_package(OpenSSL REQUIRED)
find_package(Boost REQUIRED context)

include_directories(${SQLite3_INCLUDE_DIRS} ${CURL_INCLUDE_DIRS} ${MYSQL_INCLUDE_DIR} ${OPENSSL_INCLUDE_DIR} ${PostgreSQL_INCLUDE_DIRS})

set(LIBS ${SQLite3_LIBRARIES} ${CURL_LIBRARIES} 
          ${MYSQL_LIBRARY} ${Boost_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}
          ${PostgreSQL_LIBRARIES} ${OPENSSL_LIBRARIES})

add_custom_target(
    symbols_generation
    COMMAND li_symbol_generator /source)

function(li_add_executable target_name)
  add_executable(${target_name} ${ARGN})
  target_link_libraries(${target_name} ${LIBS})
  add_dependencies(${target_name} symbols_generation)
  # target_precompile_headers(${target_name}  REUSE_FROM precompiled_header_target)
endfunction(li_add_executable)

"""

  source_files_str = "\n".join([f"/source/{path}" for path in source_files])
  script += f"li_add_executable(lithium_server {source_files_str} )\n"
  script += "target_link_libraries(lithium_server ${LIBS})\n"

  with open(output_filepath, 'w') as f:
    f.write(script)

def compile_and_run(source_files, run_args, publish_arg, build_dir = None, debug = False):
  # Get the common dir of all source files.
  source_dir = os.path.commonpath([os.path.split(f)[0] for f in source_files])

  # Compute source files path relative to source dir.
  source_files = [ os.path.relpath(f, source_dir) for f in source_files ]

  # Create the build dir if needed.
  if build_dir is None:
    build_dir = f"/tmp/lithium_build_{hash_string(source_dir)}"
    if not os.path.exists(build_dir):
      os.makedirs(build_dir)
    print("Building in ", build_dir)

  # Create cmake script.
  create_cmake_script_if_needed(source_files, build_dir)
  # Shell script to build & run.
  compile_run_script = f"cd /build && cmake . && make && ./lithium_server {' '.join(run_args)}"
  # Run the shell script in docker.
  # --rm delete the container after running.
  network = "--network host"
  if publish_arg:
      network="-p " + publish_arg
  subprocess.run(f"docker run --rm -ti {network} -v {source_dir}:/source -v {build_dir}:/build lithium_docker_image /bin/bash -c".split(" ") + [compile_run_script])


def is_cpp_file(f): 
  return os.path.splitext(f)[1] in [".cc", ".cpp"]

def gather_source_files(args):
  source_files = []
  run_args_pos = len(args)
  # Gather sources files.
  for idx, arg  in enumerate(args):
    if os.path.isdir(arg):
      source_files += [ f for f in glob.glob(f"{os.path.abspath(arg)}/**", recursive=True) if is_cpp_file(f) ]
    elif os.path.isfile(arg) and is_cpp_file(arg):
      source_files += [os.path.abspath(arg)]
    else:
      run_args_pos = idx
      break

  if len(source_files) == 0:
    print("Could not find any existing C++ files.")
    exit(1)
  return source_files, args[run_args_pos:]


def run_command(args):
  source_files, run_args = gather_source_files(args.args)
  build_docker_image_if_missing()
  compile_and_run(source_files, run_args, args.publish)
def upgrade_command(args):
  build_docker_image(no_cache=True)
def help_command(args):
  parser.print_help()  

parser = argparse.ArgumentParser(
  formatter_class=argparse.RawDescriptionHelpFormatter,
  description="""Lithium Command Line Interface
----------------------------------
  
  Important node: requires docker to run.

  This tool allows to build an run lithium server
  by simply providing C++ source file without having
  to install lithium dependency on the machine.
""")

subparsers = parser.add_subparsers(help='Available commands')

help_argparser = subparsers.add_parser('help', help="Display this message")
help_argparser.set_defaults(func=help_command)

run_argparser = subparsers.add_parser('run', help="Build and run a lithium server", description="""
Compile and run a lithium server inside a docker container.
Symbols generation will place a symbols.hh file in each directory
containing C++ files. #include "symbols.hh" is then required in order to
use lithium symbols (e.g s::symbol_name).

To save compilation output, a build directory in under /tmp of the host machine.
""",
epilog="""

Publishing the server port on non Linux hosts:

  On non linux hosts, you need to expose the server port using the -p local_machine_port:in_container_port option:
    $ li run -p 1234:8080  ./main.cc 8080
  This will make the server (running on port 8080 in the container) accessible to the local machine on port 1234.
  If the -p option is not set, docker is run with --network host which only works on linux hosts. 

Examples:
  Run the server written in file main.cc and taking as first argument the listening port
    $ li run ./main.cc 8080

  Run the server written in all C++ files contained ./directory and taking as first argument the listening port
    $ li run ./directory 8080

  Run the server written in file main.cc and utils.cc and taking as no argument
    $ li run ./main.cc ./utils.cc 8080

""",
formatter_class=argparse.RawDescriptionHelpFormatter)
run_argparser.add_argument('--publish', '-p', type=str, help="""Docker pushlish option [see bellow]
""")
run_argparser.add_argument('args', nargs="+", type=str, help='Source dir or files to compile followed by the server args.')
run_argparser.set_defaults(func=run_command)

upgrade_argparser = subparsers.add_parser('upgrade', help="Upgrade the lithium docker image")
upgrade_argparser.set_defaults(func=upgrade_command)

args = parser.parse_args()
if hasattr(args, "func"):
  args.func(args)
else:
  help_command(args)
